﻿using System;
using System.Collections;
using System.Collections.Generic;
using PhiStore.Policies;

namespace PhiStore.Generics
{
    /// <summary>
    /// A generic chunked array
    /// </summary>
    /// <typeparam name="T">Value type</typeparam>
    public class PhiArray<T> : IEnumerable<T>
    {
        ChunkedArraySettings settings;

        private PhiClient<object> client;
        private string key;
        private T[] chunk;
        private long chunkIndex = 0L;

        /// <summary>
        /// Constructor for recreating the array fromthe PhiStore. If the object doesn't exist, PhiException is thrown.
        /// </summary>
        /// <param name="client">The PhiClient to use. It must be connected!</param>
        /// <param name="keyName">The key for the array in the PhiStore</param>
        public PhiArray(PhiClient<object> client, string keyName)
        {
            this.key = keyName;
            this.client = client;
            settings = (ChunkedArraySettings)client.ListRead(key, 0L);
            chunk = (T[])client.ListRead(key, 1);
        }

        /// <summary>
        /// Constructor for creating the array. If object already exist, PhiException is thrown.
        /// Object WILL be prefetchable
        /// </summary>
        /// <param name="client">The PhiClient to use. It must be connected!</param>
        /// <param name="keyName">The key for the array in the PhiStore</param>
        /// <param name="length">length of array</param>
        public PhiArray(PhiClient<object> client, string keyName, long length)
            : this(client, keyName, length, 1L)
        { }

        /// <summary>
        /// Constructor for creating the chunked array. If object already exist, PhiException is thrown.
        /// Object WILL be prefetchable
        /// </summary>
        /// <param name="client">The PhiClient to use. It must be connected!</param>
        /// <param name="keyName">The key for the array in the PhiStore</param>
        /// <param name="length">length of array</param>
        /// <param name="chunkSize">size of chunks</param>
        public PhiArray(PhiClient<object> client, string keyName, long length, long chunkSize)
        {
            Policy p = Examples.Array;
            p.MayDelete = true;
            p.MayGarbageCollect = false;
            p.MayAppend = true;
            p.FastReturn = true;

            this.client = client;
            this.key = keyName;

            long chunks = length / chunkSize + (length % chunkSize == 0 ? 0 : 1);

            settings = new ChunkedArraySettings(length, chunkSize);

            client.ListCreate(key, new List<object>(), p); //may throw PhiException if exists

            client.ListAppend(key, settings); //1st index is settings
            for (int i = 0; i < chunks; i++)
            {
                client.ListAppend(key, new T[chunkSize]);
            }

            chunk = new T[chunkSize];
            chunkIndex = 1L;
        }

        private long CalculateChunkIndex(long index)
        {
            return 1L + index / settings.chunkSize;
        }
        /// <summary>
        /// Gets or sets an index in the array
        /// </summary>
        /// <param name="index"></param>
        /// <returns>the value / throws ArgumentOutOfRangeException if out-of-bounds</returns>
        public T this[long index]
        {
            get
            {
                if (index < 0 || index > settings.length)
                    throw new ArgumentOutOfRangeException("index too large or too small for this array");

                long cIndex = CalculateChunkIndex(index);
                if (cIndex != chunkIndex)
                {
                    chunk = (T[])client.ListRead(key, cIndex);
                    chunkIndex = cIndex;
                }

                return chunk[index % settings.chunkSize];
            }
            set
            {
                if (index < 0 || index > settings.length)
                    throw new ArgumentOutOfRangeException("index too large or too small for this array");

                long cIndex = CalculateChunkIndex(index);
                if (cIndex != chunkIndex)
                {
                    chunk = (T[])client.ListRead(key, cIndex);
                    chunkIndex = cIndex;
                }

                chunk[index % settings.chunkSize] = value;
                client.ListUpdate(key, chunk, chunkIndex);
            }
        }

        /// <summary>
        /// Gets the number of elements in the array
        /// </summary>
        public long Count
        {
            get { return settings.length; }
        }

        /// <summary>
        /// The enumerator for the PhiArray
        /// </summary>
        /// <returns></returns>
        public IEnumerator<T> GetEnumerator()
        {
            return new PhiArrayEnumerator<T>(this);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        /// <summary>
        /// Gets the key used within PhiStore
        /// </summary>
        public string KeyName
        {
            get { return key; }
        }

        /// <summary>
        /// Disconnects the internal client
        /// </summary>
        public void Disconnect()
        {
            client.Disconnect();
        }

    }

    [Serializable]
    internal class ChunkedArraySettings
    {
        public long chunkSize;
        public long length;

        public ChunkedArraySettings(long length, long chunksize)
        {
            this.length = length;
            this.chunkSize = chunksize;
        }
    }

    /// <summary>
    /// PhiArray Enumerator
    /// </summary>
    /// <typeparam name="T">Value type</typeparam>
    public class PhiArrayEnumerator<T> : IEnumerator<T>
    {
        private long index = -1L;
        private PhiArray<T> array;
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="array"></param>
        public PhiArrayEnumerator(PhiArray<T> array)
        {
            this.array = array;
        }

        /// <summary>
        /// element @ current position
        /// </summary>
        public T Current
        {
            get { return array[index]; }
        }

        /// <summary>
        /// Disposes the enumerator
        /// </summary>
        public void Dispose()
        {
            array = null;
        }
        
        object IEnumerator.Current
        {
            get { return Current; }
        }

        /// <summary>
        /// Moves the cursor one
        /// </summary>
        /// <returns>whether or not we are out of bounds</returns>
        public bool MoveNext()
        {
            index++;
            if (index < array.Count)
                return true;
            else
                return false;
        }

        /// <summary>
        /// Resets the cursor
        /// </summary>
        public void Reset()
        {
            index = -1L;
        }
    }
}
